package il.ac.tau.cvws.scrabblesidekick.server.servlet.multipart;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

public class MultipartMap extends HashMap<String, Object> 
{
	private static final long serialVersionUID = 1L;
	
	private static final String ATTRIBUTE_NAME               = "parts";
	private static final String CONTENT_DISPOSITION          = "content-disposition";
	private static final String CONTENT_DISPOSITION_FILENAME = "filename";
	private static final String DEFAULT_ENCODING             = "UTF-8";
	private static final int DEFAULT_BUFFER_SIZE             = 10240; // 10KB.

	private String encoding;

	public MultipartMap(HttpServletRequest multipartRequest, Servlet servlet) throws ServletException, IOException, ClassNotFoundException
	{
		this(multipartRequest, new MultipartConfigElement(servlet.getClass().getAnnotation(MultipartConfig.class)).getLocation(), true);
	}

	public MultipartMap(HttpServletRequest multipartRequest, String location) throws ServletException, IOException, ClassNotFoundException
	{
		this(multipartRequest, location, false);
	}

	// Global constructor.
	private MultipartMap(HttpServletRequest multipartRequest, String location, boolean multipartConfigured) throws ServletException, IOException, ClassNotFoundException
	{
		multipartRequest.setAttribute(ATTRIBUTE_NAME, this);

		this.encoding = multipartRequest.getCharacterEncoding();
		
		if (this.encoding == null) {
			multipartRequest.setCharacterEncoding(this.encoding = DEFAULT_ENCODING);
		}

		for (Part part : multipartRequest.getParts()) 
		{
			String filename = getFilename(part);
			
			if (filename == null) 
			{
				processTextPart(part);
			}
			else if (!filename.isEmpty()) 
			{
				processObjectPart(part, filename);
			}
		}
	}

	@Override
	public Object get(Object key) 
	{
		Object value = super.get(key);
		
		if (value instanceof String[]) 
		{
			String[] values = (String[]) value;
			return values.length == 1 ? values[0] : Arrays.asList(values);
		} 
		else 
		{
			return value; // Can be File or null.
		}
	}

	public String getParameter(String name) 
	{
		Object value = super.get(name);
		
		if (value instanceof File) 
		{
			return ((File) value).getName();
		}
		
		String[] values = (String[]) value;
		return values != null ? values[0] : null;
	}

	public String[] getParameterValues(String name) 
	{
		Object value = super.get(name);
		
		if (value instanceof File) 
		{
			return new String[] { ((File) value).getName() };
		}
		
		return (String[]) value;
	}

	public Enumeration<String> getParameterNames() 
	{
		return Collections.enumeration(keySet());
	}

	public Map<String, String[]> getParameterMap() 
	{
		Map<String, String[]> map = new HashMap<String, String[]>();
		
		for (Entry<String, Object> entry : entrySet()) 
		{
			Object value = entry.getValue();
			
			if (value instanceof String[]) 
			{
				map.put(entry.getKey(), (String[]) value);
			}
			else 
			{
				map.put(entry.getKey(), new String[] { ((File) value).getName() });
			}
		}
		return map;
	}

	public Object getObject(String name) 
	{
		return super.get(name);
	}

	private String getFilename(Part part) 
	{
		for (String cd : part.getHeader(CONTENT_DISPOSITION).split(";")) 
		{
			if (cd.trim().startsWith(CONTENT_DISPOSITION_FILENAME)) 
			{
				return cd.substring(cd.indexOf('=') + 1).trim().replace("\"", "");
			}
		}
		return null;
	}

	// Returns the text value of the given part.
	private String getValue(Part part) throws IOException 
	{
		BufferedReader reader =  new BufferedReader(new InputStreamReader(part.getInputStream(), encoding));
		StringBuilder value = new StringBuilder();
		
		char[] buffer = new char[DEFAULT_BUFFER_SIZE];
		
		for (int length = 0; (length = reader.read(buffer)) > 0;) 
		{
			value.append(buffer, 0, length);
		}
		
		return value.toString();
	}

	//Process given part as Text part.
	private void processTextPart(Part part) throws IOException 
	{
		String name = part.getName();
		String[] values = (String[]) super.get(name);

		if (values == null) 
		{
			// Not in parameter map yet, so add as new value.
			put(name, new String[] { getValue(part) });
		} 
		else 
		{
			// Multiple field values, so add new value to existing array.
			int length = values.length;
			String[] newValues = new String[length + 1];
			System.arraycopy(values, 0, newValues, 0, length);
			newValues[length] = getValue(part);
			put(name, newValues);
		}
	}

	private void processObjectPart(Part part, String filename) throws IOException, ClassNotFoundException 
	{
		Object obj = null;
		InputStream input = null;
		ObjectInputStream oos = null;
		
		try 
		{
			input = new BufferedInputStream(part.getInputStream());
			oos  = new ObjectInputStream(input);
			obj  = oos.readObject();
		}
		finally 
		{
			if (oos != null) try { oos.close(); } catch (IOException logOrIgnore) { /**/ }
			if (input != null) try { input.close(); } catch (IOException logOrIgnore) { /**/ }
		}

		put(part.getName(), obj);
		part.delete(); // Cleanup temporary storage.
	}

}